Skip to content

Suivi à distance : le parent consulte la progression de son enfant depuis son propre appareil - #72

Merged
isc merged 4 commits into
mainfrom
feat/suivi-a-distance
Jul 25, 2026
Merged

Suivi à distance : le parent consulte la progression de son enfant depuis son propre appareil#72
isc merged 4 commits into
mainfrom
feat/suivi-a-distance

Conversation

@isc

@isc isc commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Le besoin

Le multi-profils (§13) couvre la tablette familiale partagée. Il ne couvre pas le cas, très courant, où l'enfant pratique sur un appareil qui n'est pas celui du parent — un ancien téléphone de la famille, en WiFi seul, gardé pour lui. Le tableau de bord parent existait déjà, mais il vivait sur l'appareil de l'enfant, donc le parent n'avait aucune visibilité.

Une alternative envisagée puis écartée : le recap par e-mail. Il aurait imposé de collecter une adresse, de faire transiter les stats de l'enfant en clair sur un serveur, et d'ajouter un expéditeur à configurer — pour une information moins riche et moins fraîche que ce que donne un miroir chiffré consultable à la demande.

L'approche

Deux rôles, non exclusifs sur un même appareil.

  • Publieur (appareil de l'enfant) — après chaque séance, dépose un instantané du profil, gzippé puis chiffré côté client (AES-GCM), sous un code de 96 bits.
  • Suiveur (appareil du parent) — a scanné le QR une fois, relit le dépôt et le déchiffre localement.

C'est la primitive du transfert (§7) : la clé voyage dans le fragment d'URL, jamais transmise au serveur, qui ne voit passer qu'un blob opaque. Deux différences justifient une table distincte de transfers : le dépôt est durable (rafraîchi au lieu d'être consommé) et la lecture non consommante.

Le profil suivi n'est jamais installé chez le parent : il ne vit qu'en mémoire. Installé, il apparaîtrait dans « Qui joue ? » et une séance faite dessus par erreur divergerait de l'appareil de l'enfant, que le dépôt suivant écraserait.

Un parent qui n'a pas encore Tablito

Pris en charge de bout en bout : le fragment #watch= saute la landing statique, et l'app ouvre directement l'espace parent avec la progression de l'enfant. Aucun onboarding enfant (prénom, test de placement) ne lui est proposé — il n'est pas venu s'entraîner. Une porte de sortie « Créer un profil sur cet appareil » reste disponible s'il veut aussi le sien.

Les deux sources en parallèle

Un sélecteur en tête de l'espace parent bascule entre le profil local (qui continue de vivre) et chaque enfant suivi. Il n'apparaît que s'il y a vraiment un choix. Le corps de stats est extrait en ParentStats pour que la vue distante soit exactement le même rendu que la locale. Une barre de fraîcheur indique la date du dernier dépôt (« Synchronisé il y a 2 h ») : sans elle, un appareil éteint depuis une semaine afficherait des chiffres périmés sans le dire.

Sécurité et confidentialité

Le code est une capacité permanente jusqu'à révocation. Garde-fous : 96 bits (inénumérable), clé jamais exposée au serveur (un dump de la base ne donne rien de lisible), « Ne plus partager » qui supprime le dépôt immédiatement, purge automatique après 6 mois sans rafraîchissement.

La page Confidentialité gagne une section (fr/en) : le vrai delta à déclarer n'est pas le chiffrement (identique au transfert) mais la durée. Les quatre endroits qui affirmaient « les données restent sur l'appareil » — dont la meta description, donc le SEO — gagnent « par défaut », ce qui les rend exacts (c'était déjà approximatif avec le transfert et le rappel quotidien).

Structure

  • supabase/watches.sql — table + 3 RPC SECURITY DEFINER, RLS activée sans policy, index sur updated_at
  • lib/watchStore.ts (localStorage, eager) / lib/watch.ts (réseau + crypto, chargé à la demande) — la séparation évite que le chiffrement entre dans le graphe de modules du boot
  • supabaseRpc() remonté dans lib/supabase.ts et adopté par transfer.ts, qui en avait deux copies open-codées
  • scanner QR extrait en hook useQrScan, réutilisé par WelcomeScreen ; QR rendu par QrCanvas
  • specs §14, entrée de changelog

Vérifications

  • 271 tests verts (watch.test.ts : chiffrement, durabilité, révocation, non-installation ; remoteFollow.test.tsx : boot suiveur-only, masquage des sections locales, sélecteur de source, sortie d'onboarding)
  • tsc -b clean, build prod OK
  • graphe de modules eager vérifié sur le build : watch et transfer restent lazy, 81 modules eager contre 79 avant (les 2 ajoutés sont watchStore, voulu, et useQrScan, inévitable puisque WelcomeScreen est eager)

À faire avant déploiement

psql "$SUPABASE_DB_URL" -f supabase/watches.sql sur l'instance — le projet n'a pas de système de migrations, la DDL s'applique à la main.

Suite

La notification hebdomadaire au parent (« le recap de Zoé est prêt ») part dans une PR séparée : elle demande des colonnes en plus sur push_subscriptions et de la logique cron, et sa version propre garde un corps de notif générique — le Service Worker ne peut pas lire localStorage, donc pas de déchiffrement de son côté.

🤖 Generated with Claude Code

isc added 2 commits July 25, 2026 14:27
… appareil

Le multi-profils couvre la tablette familiale partagée, mais pas le cas où
l'enfant pratique sur un appareil qui n'est pas celui du parent (ancien
téléphone en WiFi seul). Le tableau de bord parent existait déjà, mais il
vivait sur l'appareil de l'enfant.

L'appareil de l'enfant publie après chaque séance un instantané de son profil
gzippé puis chiffré côté client (AES-GCM), sous un code de 96 bits. L'appareil
du parent scanne le QR une fois et relit ce dépôt. Même primitive que le
transfert : la clé ne vit que dans le fragment d'URL, jamais transmise au
serveur. Deux différences, qui justifient une table à part : le dépôt est
durable et la lecture non consommante.

Le profil suivi n'est jamais installé chez le parent — installé, il
apparaîtrait dans « Qui joue ? » et une séance faite dessus par erreur
divergerait de l'appareil de l'enfant.

Un parent sans Tablito est pris en charge de bout en bout : le fragment
#watch= saute la landing statique et l'app ouvre directement l'espace parent,
jamais l'onboarding enfant. L'espace parent affiche les deux sources en
parallèle (profil local + enfants suivis) via un sélecteur en tête, le corps
de stats étant extrait en ParentStats pour un rendu identique des deux côtés.

- supabase/watches.sql : table + 3 RPC SECURITY DEFINER, RLS sans policy
- lib/watchStore (localStorage, eager) / lib/watch (réseau+crypto, dynamique)
  pour ne pas faire entrer le chiffrement dans le graphe de modules du boot
- supabaseRpc() remonté dans lib/supabase, adopté par transfer.ts
- scanner QR extrait en hook useQrScan, réutilisé par WelcomeScreen
- confidentialité (fr/en) : nouvelle section, 4e exception déclarée ; specs §14
- « par défaut » ajouté aux affirmations « les données restent sur l'appareil »
…en code stable

npm run lint (lancé par la CI) refusait deux choses dans ParentDashboard :
l'écriture d'une ref pendant le render (react-hooks/refs) et un setState
atteignable depuis un effet (react-hooks/set-state-in-effect).

La relecture d'un suivi chaîne désormais sa promesse INLINE dans l'effet, forme
que la règle accepte et déjà employée par NotificationSettings. L'état
« chargement » n'est plus posé au changement de source : l'absence d'instantané
pour la source affichée EST le chargement, donc il se dérive. « Actualiser » le
pose explicitement, mais depuis un handler d'événement, où c'est légitime.

La source sélectionnée repasse d'un objet à un code (string | null, null =
progression locale) : c'est l'identité stable dont dépend l'effet, donc
re-cliquer l'onglet courant ne relance plus rien — un objet recréé à chaque
render annulait la relecture en vol sans la redémarrer.
@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Preview supprimée (PR fermée). Les URLs ne sont plus accessibles.

isc added 2 commits July 25, 2026 14:47
… appareil vierge

Un parent sans Tablito qui scanne un QR expiré ou révoqué n'a ni profil local ni
suivi mémorisé. L'échec d'appairage forçait bien l'écran 'parent', mais le garde
de rendu exigeait l'un des deux — donc rien ne s'affichait, sans aucun chemin de
récupération alors que l'espace parent permet précisément de réessayer.

Le garde tient désormais compte de watchPairing, y compris quand il vaut
'error'. Test de non-régression sur un appareil totalement vierge.
…suiveur

Le chemin « parent qui découvre Tablito par le QR de son enfant, puis se crée un
profil pour lui-même » n'était vérifié que par morceaux (présence du bouton,
sortie d'onboarding possible, appareil mixte seedé à la main). Ce test joue le
parcours complet et vérifie que la création n'a pas chassé le suivi.
@isc
isc merged commit 1416876 into main Jul 25, 2026
2 checks passed
@isc
isc deleted the feat/suivi-a-distance branch July 25, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant